package com.java.security.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.java.security.entity.User;
import com.java.security.filter.LoginFilter;
import com.java.security.service.UserService;
import com.java.security.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

import java.io.PrintWriter;

/**
 * @author jl
 * Created on 2020/7/29
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//    @Autowired
//    VerifyCodeFilter verifyCodeFilter;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 放行的资源配置
     * web.ignoring() 用来配置忽略掉的 URL 地址，一般对于静态文件，我们可以采用此操作。
     * 这里的请求不会经过的 Spring Security 过滤器链
     */
    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/css/**", "/js/**", "/index.html", "/img/**", "/fonts/**", "/favicon.ico", "/verifyCode", "/vc.jpg");
    }

    /**
     * 配置登录项
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 配置403没有访问权限的跳转页面
        http.exceptionHandling().accessDeniedPage("/unauth.html");
        // 添加验证码过滤器 (LoginFilter包含了验证码过滤)
//        http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
        http.authorizeRequests()
                // 设置静态的资源允许所有访问
                .antMatchers("/static/**").permitAll()
                .antMatchers("/admin/**").hasRole("admin")  // 需要admin角色
                .antMatchers("/user/**").hasRole("user") // 需要user角色
                // 其他所有资源都需要认证(登陆)后才能访问
                .anyRequest().authenticated()
                .and()
                /* 登录配置 */
                // 设置默认登陆页面/login
                .formLogin()
                .loginPage("/login.html")  // 当我们定义了登录页面为 /login.html 且没有定义登录接口地址的时候，Spring Security 会帮我们自动注册一个 /login.html 的接口，这个接口是 POST 请求，用来处理登录逻辑。
                .loginProcessingUrl("/doLogin") // 定义登录接口地址
                .usernameParameter("username") // 默认为username,自定义须和登录表单中的参数保持一致
                .passwordParameter("password") // 默认为password,自定义须和登录表单中的参数保持一致
//                .defaultSuccessUrl("/index") // 重定向登录成功的跳转URL,登录之前有别的请求(如/hello)会跳转到之前的请求url(/hello)
//                .successForwardUrl("/index") // 表示不管你是从哪里来的，登录成功后一律跳转到指定URL (实际操作中，defaultSuccessUrl 和 successForwardUrl 只需要配置一个即可).defaultSuccessUrl 还有一个重载的方法，第二个参数如果不设置默认为 false，也就是我们上面的的情况，如果手动设置第二个参数为 true，则 defaultSuccessUrl 的效果和 successForwardUrl 一致。
                .successHandler((req, resp, authentication) -> {
                    com.java.security.entity.User user = (com.java.security.entity.User) authentication.getPrincipal();
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    R r = R.ok();
                    r.put("username", user.getUsername());
                    out.write(new ObjectMapper().writeValueAsString(r));
                    out.flush();
                    out.close();
                })
//                .failureUrl("/login.html") // 在登录失败之后，会发生重定向
                .failureHandler((req, resp, e) -> {
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    R r = R.error();
                    if (e instanceof LockedException) {
                        r.put("msg", "账户被锁定，请联系管理员!");
                    } else if (e instanceof CredentialsExpiredException) {
                        r.put("msg", "密码过期，请联系管理员!");
                    } else if (e instanceof AccountExpiredException) {
                        r.put("msg", "账户过期，请联系管理员!");
                    } else if (e instanceof DisabledException) {
                        r.put("msg", "账户被禁用，请联系管理员!");
                    } else if (e instanceof BadCredentialsException) {
                        r.put("msg", "用户名或者密码输入错误，请重新输入!");
                    }
                    out.write(new ObjectMapper().writeValueAsString(r));
                    out.flush();
                    out.close();
                })
                .permitAll()  // 表示相关的页面/接口不要被拦截
                .and() // and 方法表示结束当前标签，上下文回到HttpSecurity，开启新一轮的配置
                /* 用户无权限返回值设置 */
                .exceptionHandling()
                .accessDeniedHandler((req, resp, authentication) -> {
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    R r = R.error(403, "您没有权限访问当前资源");
                    out.write(new ObjectMapper().writeValueAsString(r));
                    out.flush();
                    out.close();
                })
                .and()
                /* 注销配置 */
                .logout()
                .logoutUrl("/logout") // 默认注销的 URL 是 /logout，是一个 GET 请求，我们可以通过 logoutUrl 方法来修改默认的注销 URL
//                .logoutRequestMatcher(new AntPathRequestMatcher("/logout","POST")) // logoutRequestMatcher 方法不仅可以修改注销 URL，还可以修改请求方式，实际项目中，这个方法和 logoutUrl 任意设置一个即可
//                .logoutSuccessUrl("/index") // logoutSuccessUrl 表示注销成功后要跳转的页面。默认/login?logout
                // 默认注销后悔跳转.在前后分离中重写该方法返回JSON让前端处理跳转
                .logoutSuccessHandler((req, resp, authentication) -> {
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    R r = R.ok("注销成功");
                    out.write(new ObjectMapper().writeValueAsString(r));
                    out.flush();
                    out.close();
                })
                .deleteCookies() // 清除 cookie
                .clearAuthentication(true) // 清除认证信息, 默认为true
                .invalidateHttpSession(true) // 使 HttpSession 失效, 默认为true
                .permitAll()
                .and()
                .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .and()
                .exceptionHandling()
                // LoginUrlAuthenticationEntryPoint.commence()未认证就访问数据，默认直接重定向到登录页面.在前后分离中重写该方法返回JSON让前端处理跳转
                .authenticationEntryPoint((req, resp, authException) -> {
                            resp.setContentType("application/json;charset=utf-8");
                            PrintWriter out = resp.getWriter();
                            R r = R.error("尚未登录，请先登录");
                            out.write(new ObjectMapper().writeValueAsString(r));
                            out.flush();
                            out.close();
                        }
                )
                .and()
                // 表示配置最大会话数为 1，这样后面的登录就会自动踢掉前面的登录
                .sessionManagement().maximumSessions(1);
        // 调用 addFilterAt 方法完成替换UsernamePasswordAuthenticationFilter操作
        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Autowired
    private UserService userService;

    /**
     * 重写 configure 方法，
     * 只不过这次我们不是基于内存，也不是基于 JdbcUserDetailsManager，而是使用自定义的 UserService
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    /**
     * 角色继承:上级可具备下级的所有权限
     * 注意，在配置时，需要给角色手动加上 ROLE_ 前缀
     */
    @Bean
    RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
        hierarchy.setHierarchy("ROLE_admin > ROLE_user");
        return hierarchy;
    }

    /**
     * 过滤器定义完成后，接下来用我们自定义的过滤器代替默认的 UsernamePasswordAuthenticationFilter
     * 首先我们需要提供一个 LoginFilter 的实例
     * 当我们代替了 UsernamePasswordAuthenticationFilter 之后，
     * 原本在 SecurityConfig#configure 方法中关于 form 表单的配置就会失效，那些失效的属性，都可以在配置 LoginFilter 实例的时候配置
     */
    @Bean
    LoginFilter loginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
            User user = (User) authentication.getPrincipal();
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            R r = R.ok();
            r.put("username", user.getUsername());
            out.write(new ObjectMapper().writeValueAsString(r));
            out.flush();
            out.close();
        });
        loginFilter.setAuthenticationFailureHandler((request, response, e) -> {
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            R r = R.error();
            if (e instanceof LockedException) {
                r.put("msg", "账户被锁定，请联系管理员!");
            } else if (e instanceof CredentialsExpiredException) {
                r.put("msg", "密码过期，请联系管理员!");
            } else if (e instanceof AccountExpiredException) {
                r.put("msg", "账户过期，请联系管理员!");
            } else if (e instanceof DisabledException) {
                r.put("msg", "账户被禁用，请联系管理员!");
            } else if (e instanceof BadCredentialsException) {
                r.put("msg", "用户名或者密码输入错误，请重新输入!");
            } else if (e instanceof AuthenticationException) {
                r.put("msg", "验证码错误!");
            }
            out.write(new ObjectMapper().writeValueAsString(r));
            out.flush();
            out.close();
        });
        loginFilter.setAuthenticationManager(authenticationManagerBean());
        loginFilter.setFilterProcessesUrl("/doLogin");
        return loginFilter;
    }


}